在說明 Promise 前,首先我們要知道為什麼需要有 Promise。我們知道 setTimeout() 是屬於非同步的一種,如果我們需要第一個執行完成後才執行第二個,以此類推,那我們可能會怎麼寫?
setTimeout(() => {
console.log("我是第一個");
setTimeout(() => {
console.log("我是第二個");
setTimeout(() => {
console.log("我是第三個");
setTimeout(() => {
console.log("我是第四個");
setTimeout(() => {
console.log("我是第五個");
}, 1000);
}, 1000);
}, 1000);
}, 1000);
}, 1000);
由上面可知,這樣的寫法雖然執行上沒有問題,但非常難閱讀及容易形成傳說中的波動拳 code,也容易造成所謂的 回呼地獄 / 回調地獄 (Callback Hell)
,所以 Promise 就誕生了。
圖片 ケン 取自 CAPCOM
// Callback Hell
asyncFunction1((result1) => {
asyncFunction2(result1, (result2) => {
asyncFunction3(result2, (result3) => {
// 更多 callback function ...
});
});
});
Promise 是一種用於處理非同步操作的工具,它能夠幫助我們更有效地處理非同步的程式邏輯。它用於執行一些需要等待時間的操作,例如網路請求、檔案讀寫等,並在操作完成後返回結果或錯誤,可以有三種狀態:pending (進行中)
、fulfilled (已完成)
和 rejected (已拒絕)
,然後會使用 resolve 回傳成功結果
或 reject 回傳失敗的錯誤
。
我們假設娜美和索隆是情侶,娜美對索隆說:「如果這次成功懷孕,我們就結婚,如果失敗,那我要恢復單身讓香吉士來追求,不過不管最後結局如何,我們還是會一起去攻打黑鬍子海賊團。」
但確認有沒有懷孕總是需要一點時間吧!所以索隆跟娜美很適合當 Promise 的範例,展示了如何創建一個處理非同步操作的 Promise,並使用 Promise chain 的 .then()
、.catch()
和 .finally()
來處理操作的結果和錯誤。
看以下範例:
const namiPregnant = (): Promise<string> => {
return new Promise((resolve, reject) => {
const success = Math.random() >= 0.5; // 50% 的機率
if (success) resolve("我們結婚吧。"); // 成功
reject(new Error("我們分手吧。")); // 失敗
});
}
namiPregnant()
.then((result) => {
console.log(result); // 輸出: 我們結婚吧!
})
.catch((error) => {
console.error(error.message); // 輸出: 我們分手吧!
})
.finally(() => {
console.log("攻打黑鬍子海賊團。"); // 輸出: 攻打黑鬍子海賊團。
});
在這個範例中,我們設定 namiPregnant 函數返回一個 Promise,該 Promise 隨機模擬懷孕成功或失敗,使用 .then()
來處理懷孕成功的情況,並使用 .catch()
來處理懷孕失敗的情況,無論娜美懷孕成功或失敗,使用 .finally()
來處理最後索隆和娜美還是會一起去攻打黑鬍子海賊團的情況。
不過呢,如果我們設定的 target 為 ES5 以下,在這邊我們會遇到 vscode 跳出錯誤警告,如下圖:
這是因為 Promise 為 ECMAScript 6 (ES2015) 新增的標準 API,而 finally() 為 ECMAScript 9 (ES2018) 新增,所以我們有兩種解決方式:
tsconfig.json 的 target 須為 ES6 之後的版本
,如果會用到 finally(),就必須為 ES9 之後的版本。// tsconfig.json
{
"compilerOptions": {
// ... ,
"target": "ES2018"
// ...,
}
}
不過會將 target 修改為 ES5 就是為了要支援舊版本瀏覽器,所以我們還有第二種解法。
tsconfig.json 的 lib 為 ES6 之後的版本
,如果會使用到 finally(),就必須為 ES9 之後的版本。// tsconfig.json
{
"compilerOptions": {
// ... ,
"target": "ES5",
"lib": ["ES2018", "DOM"]
// ...,
}
}
以上述兩種方式都可以成功消除 TypeScript 的紅波浪警告,就看哪一種方式適合專案當前的狀態。
我們再看另一個使用 Promise 的範例,這次使用箭頭函示並傳入參數,同樣模擬了非同步讀取文件的情境:
const promiseFile = (filename: string): Promise<string> => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (filename === "example.txt") resolve("檔案讀取成功"); // 成功
reject(new Error("找不到檔案")); // 失敗
}, 1000);
});
};
promiseFile("example.txt")
.then((data) => {
console.log(data); // 輸出: 檔案讀取成功
})
.catch((error) => {
console.error("Error:", error.message); // 輸出: 找不到檔案
});
在這個範例中,我們設定 promiseFile 函式返回一個 Promise,該 Promise 在一秒後隨機模擬操作成功或失敗,使用 .then()
方法來處理操作成功的情況,並使用 .catch()
方法來處理操作失敗的情況。這樣,無論操作成功還是失敗,我們都能夠適當地處理。
Promise 還支持鏈式操作,這使我們能夠在多個非同步操作之間建立清晰的流程。
我們可以用上面的波動拳 code 改成使用 Promise chain 來當範例:
const promiseUser = (id: number): Promise<string> => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id) resolve(`我是第 ${id} 個`);
reject(new Error("沒有 id"));
}, 1000);
});
};
promiseUser(1)
.then((res) => {
console.log(res); // 輸出: 我是第 1 個
return promiseUser(2);
})
.then((res) => {
console.log(res); // 輸出: 我是第 2 個
return promiseUser(3);
})
.then((res) => {
console.log(res); // 輸出: 我是第 3 個
return promiseUser(4);
})
.then((res) => {
console.log(res); // 輸出: 我是第 4 個
return promiseUser(5);
})
.then((res) => {
console.log(res); // 輸出: 我是第 5 個
})
.catch((error) => {
console.error(error.message);
});
在這個範例中,首先使用 promiseUser() 一秒後取得資料,然後使用 .then() 接收第一筆資料的回傳,再執行第二次操作 promiseUser(),接著再次使用 .then() 取得第二筆資料的回傳再執行第三次操作,以此類推。這種方式就能幫助我們建立一個更明確的流程,使程式碼更具可讀性。
我們再看一個使用 Promise chain 的範例:
interface IUserData {
id: number;
username: string;
}
const promiseUserData = (userId: number): Promise<IUserData> => {
return new Promise((resolve, _) => {
setTimeout(() => {
const user = { id: userId, username: "威爾豬" };
resolve(user);
}, 1000);
});
}
const promisePosts = (user: IUserData): Promise<string[]> => {
return new Promise((resolve, _) => {
setTimeout(() => {
const posts = [
`貼文 1 by ${user.username}`,
`貼文 2 by ${user.username}`,
];
resolve(posts);
}, 1000);
});
}
promiseUserData(1)
.then((user) => {
console.log(user); // 輸出: {id: 1, username: '威爾豬'}
return promisePosts(user);
})
.then((posts) => {
console.log(posts); // 輸出: ['貼文 1 by 1', '貼文 2 by 1']
})
.catch((error) => {
console.error(error.message);
});
在這個範例中,我們首先使用 promiseUserData 函式獲取使用者數據,然後使用 .then() 在使用者數據獲取後執行操作。接著,我們使用 promisePosts 函式獲取使用者的貼文,並再次使用 .then() 處理貼文數據,這樣非同步就會 依序出現
,先輸出使用者,一秒後再輸出貼文。
等全部完成後再同時進行回傳
。當然我們可以將上面範例改成使用 Promise.all 來處理多個非同步操作:
interface IUserData {
id: number;
username: string;
}
const promiseUserData = (user: IUserData): Promise<IUserData> => {
return new Promise((resolve, _) => {
setTimeout(() => {
resolve(user);
}, 1000);
});
};
const promisePosts = (user: IUserData): Promise<string[]> => {
return new Promise((resolve, _) => {
setTimeout(() => {
const posts = [
`貼文 1 by ${user.username}`,
`貼文 2 by ${user.username}`,
];
resolve(posts);
}, 1000);
});
};
const promiseAllData = () => {
const user = { id: 1, username: "威爾豬" };
Promise.all([promiseUserData(user), promisePosts(user)])
.then(([user, posts]) => {
console.log(user); // 輸出: {id: 1, username: '威爾豬'}
console.log(posts); // 輸出: ['貼文 1 by 1', '貼文 2 by 1']
})
.catch((error) => {
console.error("Error:", error.message);
});
};
promiseAllData();
我們直接使用 Promise.all 來同時操作 promiseUserData 和 promisePosts 這兩個 Promise 的函式,使用 .then() 和 .catch() 處理回傳的成功結果和錯誤,並運用解構來提取 Promise.all 返回陣列中的 user 和 posts,這樣 user 和 posts 就會 同時出現
,當然陣列顯示的結果順序會與一開始傳入的順序一樣 ( 先 user 後 posts ),我們就能夠使用這些結果進行後續處理。
只回傳第一個完成的
。使用 Promise.race 可以解決一些需要快速回應的情況,例如超時處理,或者只關心最快完成的操作的情況。
以下範例:
interface IData {
userId: number;
id: number;
title: string;
completed: boolean;
}
const promiseTimeout = (url: string, timeout: number) => {
return Promise.race([
fetch(url), // 嘗試發出網絡請求
new Promise((_, reject) =>
setTimeout(() => reject(new Error("請求已超過時間!")), timeout)
), // 超時拒絕的 Promise
]);
};
// 超時 3 秒沒回應就回傳錯誤
promiseTimeout("https://jsonplaceholder.typicode.com/todos/1", 3000)
.then((res: any) => res.json())
.then((data: IData) => console.log(data))
.catch((error) => console.error(error.message));
在這個範例中,promiseTimeout 函式使用 Promise.race 同時監聽網絡請求和一個定時器。如果網絡請求在指定的超時時間內完成,則 Promise.race 返回該請求的 Promise。如果網絡請求未能在時間內完成,則定時器的 Promise 將回傳請求拒絕的錯誤,表示請求已超時。
Promise 提供了一種更結構化和可讀性的方式來處理非同步操作,並可以解決回呼地獄的問題,使程式碼更易於維護。在實際開發中,會常看到使用 Promise 的方式,或是另外一種 async / await 的方式來取得非同步的資料,這我們後面的章節再說明。